/*
 * Demoiselle Framework
 * Copyright (C) 2011 SERPRO
 * ----------------------------------------------------------------------------
 * This file is part of Demoiselle Framework.
 * 
 * Demoiselle Framework is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License version 2
 * as published by the Free Software Foundation.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not,  see <http://www.gnu.org/licenses/>
 * or write to the Free Software Foundation, Inc., 51 Franklin Street,
 * Fifth Floor, Boston, MA  02110-1301, USA.
 * ----------------------------------------------------------------------------
 * Este arquivo é parte do Framework Demoiselle.
 * 
 * O Framework Demoiselle é um software livre; você pode redistribuí-lo e/ou
 * modificá-lo dentro dos termos da Licença Pública Geral GNU como publicada pela Fundação
 * do Software Livre (FSF); na versão 2 da Licença.
 * 
 * Este programa é distribuído na esperança que possa ser útil, mas SEM NENHUMA
 * GARANTIA; sem uma garantia implícita de ADEQUAÇÃO a qualquer MERCADO ou
 * APLICAÇÃO EM PARTICULAR. Veja a Licença Pública Geral GNU/GPL em português
 * para maiores detalhes.
 * 
 * Você deve ter recebido uma cópia da Licença Pública Geral GNU, sob o título
 * "LICENCA.txt", junto com esse programa. Se não, acesse o Portal do Software Público
 * Brasileiro no endereço www.softwarepublico.gov.br ou escreva para a Fundação do Software
 * Livre (FSF) Inc., 51 Franklin St, Fifth Floor, Boston, MA 02111-1301, USA.
 
 * @author Tarcizio Vieira Neto (tarcizio.vieira@owasp.org) <a href="http://www.serpro.gov.br">SERPRO</a>
 * @created 2011
 */
package br.gov.frameworkdemoiselle.security.accesscontrol;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.owasp.esapi.ESAPI;
import org.owasp.esapi.Logger;
import org.owasp.esapi.User;
import org.owasp.esapi.errors.AccessControlException;
import org.owasp.esapi.errors.EncodingException;
import org.owasp.esapi.errors.IntrusionException;
import org.owasp.esapi.errors.ValidationException;

/**
 * This class exists for backwards compatibility with the AccessController 1.0 
 * reference implementation.
 *
 * <P>
 * There is a single configuration file supporting each of the five methods in
 * the AccessController interface. These files are located in the ESAPI
 * resources directory as specified when the JVM was started. The use of a
 * default deny rule is STRONGLY recommended. The file format is as follows:
 * 
 * <pre>
 * resource    | opperation        | role,role   | allow/deny | comment
 * ------------------------------------------------------------------------------------
 * resource1   | opperation1       | user,admin  | allow      | authenticated users can access /banking
 * resource2   | *                 |  admin      | allow      | only admin role can access /admin
 * resource3   | opperation2       | any         | deny       | default deny rule
 * </pre>
 * 
 * 
 * @author Tarcizio Vieira Neto (tarcizio.vieira@owasp.org) <a href="http://www.serpro.gov.br">SERPRO</a>
 * @since October 1, 2011
 */
public class FileBasedACRs {
	
	/** The logger. */
	private Logger logger = ESAPI.getLogger("ResourceBasedACRs");
	
    /**
	* TODO Javadoc
	*/
    Map<String,Map> rulesApplicableForUser = new HashMap<String,Map>();
    
    /**
	* TODO Javadoc
	*/
    Map<String,List<Rule>> rulesByRole = null; 
	
    public boolean isAuthorizedForResource(String resource, String operation) throws AccessControlException{
    	
    	if (rulesByRole == null) {
    		rulesByRole = loadResourceRules("ResourceAccessRules.txt");
    	}
    	
    	User user = ESAPI.authenticator().getCurrentUser();
		Set roles = user.getRoles();
		
		Map<String, Rule> rulesApplicableForRoles = null;		
		
		if (rulesApplicableForUser.containsKey(user.getAccountName())) {
			rulesApplicableForRoles = rulesApplicableForUser.get(user.getAccountName()); 
		} else {
			rulesApplicableForRoles = new HashMap<String, Rule>();			
			
			List rulesForAny = rulesByRole.get("any");
			
			if ((rulesForAny != null) && !rulesForAny.isEmpty())
				addRuleForMap(rulesApplicableForRoles, rulesForAny);
							
			Iterator it = roles.iterator();
			while (it.hasNext()){
				String role = (String) it.next();
				addRuleForMap (rulesApplicableForRoles, rulesByRole.get(role));
			}
			
			rulesApplicableForUser.put(user.getAccountName(), rulesApplicableForRoles);
		}
		
		if (rulesApplicableForRoles == null)
			return false;
				
		boolean anyGroupAllow = false;
		boolean anyGroupDisallow = false;		
		
		for (Rule rule : rulesApplicableForRoles.values()) {
			
			if (overlap(rule.resource, resource) && overlap(rule.operation, operation)) {
				if ((rule.roles.contains("any") || resource.equals("*") || operation.equals("*"))) {
					if (!rule.allow)
						anyGroupDisallow = true;
					else anyGroupAllow = true;
				} else {	 
					return (rule.allow);  
				}				
			}
			
		}
		return anyGroupAllow && !anyGroupDisallow;
    }  	
	
	private void addRuleForMap(Map<String,Rule> map, List<Rule> ruleList) {
		for (Rule rule : ruleList) {
			map.put(rule.getKey(), rule);
		}
	}
	
	/**
	 * This method merely checks to see if ruleActions contains the action requested.
	 * 
	 * @param ruleActions
	 *      actions listed for a rule
	 * @param action
	 *      the action requested that will be searched for in ruleActions
	 * 
	 * @return 
	 * 		true, if any action exists in ruleActions.  False otherwise.
	 */
	/*
	private boolean overlap( List ruleActions, String action){
		if( ruleActions.contains(action) )
			return true;
		return false;
	}*/
	
	private boolean overlap( String rule, String resourceOrOpperation){
		if( rule.equals("*") )
			return true;
		return rule.equals(resourceOrOpperation);
	}
		
	/**
	 * Checks that the roles passed in contain only letters, numbers, and underscores.  Also checks that
	 * roles are no more than 10 characters long.  If a role does not pass validation, it is not included in the 
	 * list of roles returned by this method.  A log warning is also generated for any invalid roles.
	 * 
	 * @param roles
	 * 		roles to validate according to criteria started above
	 * @return
	 * 		a List of roles that are valid according to the criteria stated above.
	 * 
	 */
	private List validateRoles(List roles){
		List ret = new ArrayList();	
		for(int x = 0; x < roles.size(); x++){
			String canonical = ESAPI.encoder().canonicalize(((String)roles.get(x)).trim());

			if(!ESAPI.validator().isValidInput("Validating user roles in FileBasedAccessController", canonical, "RoleName", 20, false)) {
				logger.warning( Logger.SECURITY_FAILURE, "Role: " + ((String)roles.get(x)).trim() + " is invalid, so was not added to the list of roles for this Rule.");
			} else { 
				ret.add(canonical.trim());
			}
		}
		return ret;
	}
	
		
	/**
	 * Loads access rules by storing them in a hashmap.  This method begins reading the File specified by
	 * the ruleset parameter, ignoring any lines that begin with '#' characters as comments.  Sections of the access rules file
	 * are split by the pipe character ('|').  The method then loads all Classes, loads the list of comma separated roles, then the list of comma separated actions.  
	 * The roles are validated to be sure they are within a length and character set, specified in the validateRoles(String) method.  
	 * 
	 * Each path may only appear once in the access rules file.  Any entry, after the first, containing the same path will be logged and ignored. 
	 *  
	 * @param ruleset
	 *      the name of the data that contains access rules
	 * 
	 * @return 
	 * 		a hash map containing the ruleset
	 */
	private Map<String,List<Rule>> loadResourceRules(String ruleset) {
		Map<String,List<Rule>> resultMap = new HashMap<String,List<Rule>>();
				
		Map map = new HashMap();
		InputStream is = null;

		try {
			ruleset = "fbac-policies/" + ruleset;
			is = ESAPI.securityConfiguration().getResourceStream(ruleset);
			String line = "";
			while ((line = ESAPI.validator().safeReadLine(is, 500)) != null) {
				if (line.length() > 0 && line.charAt(0) != '#') {
					Rule rule = new Rule();
					String[] parts = line.split("\\|");
					
					rule.resource = getValidResource (parts[0].trim());
					
					rule.operation = getValidOpperation (parts[1].trim());
										
					List roles = commaSplit(parts[2].trim().toLowerCase());
					roles = validateRoles(roles);
					for(int x = 0; x < roles.size(); x++)
						rule.roles.add(((String)roles.get(x)).trim());
					
					String action = parts[3].trim();
					rule.allow = action.equalsIgnoreCase("allow");
					
					String mapKey = getRuleKey (rule.resource, rule.operation);
					
					if (map.containsKey(mapKey)) {
						logger.warning( Logger.SECURITY_FAILURE, "Problem in access control file. Duplicate rule ignored: " + rule);
					} else {
						map.put(mapKey, rule);
						
						Iterator it = rule.roles.iterator();
						while (it.hasNext()) {
							String role = (String) it.next();
														
							List<Rule> ruleList = null;
							if (resultMap.containsKey(role)) {
								ruleList = resultMap.get(role);
							} else {
								ruleList = new ArrayList<Rule>();
								resultMap.put(role, ruleList);
							}
							ruleList.add(rule);
													
						}
						
					}
				}
			}
		} catch (Exception e) {
			logger.warning( Logger.SECURITY_FAILURE, "Problem in access control file : " + ruleset, e );
		} finally {
			
			try {
				if (is != null) {
					is.close();
				}
			} catch (IOException e) {
				logger.warning(Logger.SECURITY_FAILURE, "Failure closing access control file : " + ruleset, e);
			}
		}
		return resultMap;
	}
			
	public String getValidResource(String toValidate) throws ValidationException, IntrusionException {
		//return ESAPI.validator().getValidInput("", toValidate, "Validator.", 512, false);
		return toValidate;
	}
	
	public String getValidOpperation(String toValidate) throws ValidationException, IntrusionException {
		//return ESAPI.validator().getValidInput("", toValidate, "Validator.", 512, false);
		return toValidate;
	}

	/**
	 * This method splits a String by the ',' and returns the result as a List.
	 * 
	 * @param input
	 * 		the String to split by ','
	 * @return
	 * 		a List where each entry was on either side of a ',' in the original String
	 */
	private List commaSplit(String input){
		String[] array = input.split(",");
		return Arrays.asList(array);
	}
	
	public static String getRuleKey (String resource, String opperation) {
		return resource + ":" + opperation;
	}
	
	/**
	 * The Class Rule.
	 */
	private class Rule {

		
		protected String resource = "";
		
		protected String operation = "";
		
		protected Set roles = new HashSet();
		
		protected boolean allow = false;				

		/**
		 * 
		 * Creates a new Rule object.
		 */
		protected Rule() {
			// to replace synthetic accessor method
		}
		
		public String getKey() {
			return getRuleKey (this.resource, this.operation);
		}		

		/**
	     * {@inheritDoc}
		 */
		public String toString() {
			return "" + resource + " | "+ operation+" | " + roles + " | " + (allow ? "allow" : "deny");
		}
	}
}
